<?php

#<editor-fold defaultstate="collapsed" desc="CCore::Using">
CCore::Using('socket.inc.abstract');
CCore::Using('socket.imap.enumerations');
CCore::Using('socket.imap.config');
CCore::Using('base.log');
#</editor-fold>

class CCoreSocketImap extends ACoreSocket
{
	#<editor-fold defaultstate="collapsed" desc="contants">
	const TAG_NAME = 'TAG';
	#</editor-fold>
	#<editor-fold defaultstate="collapsed" desc="private">
	/**
	 * @var	int
	 */
	private $_iTagCount;

	/**
	 * @var	array
	 */
	private $_aCapabilityItems;
	#</editor-fold>
	
	/**
	 * @param	CCoreSocketImapConfig	$oImapConfig
	 * @param	array					$aFollowers = array()
	 * @return	CCoreSocketImap
	 */
	public function __construct(CCoreSocketImapConfig $oImapConfig, $aFollowers = array())
	{
		CCore::CheckTypes(array('$oImapConfig', '$noInputLogger'));

		$oDevelopments = new CCoreBaseDevelopments('IMAP', 'IMAP[Error]', 'IMAP[Warning]', 'IMAP[Notice]');
		$oDevelopments->AddArray($aFollowers);

		parent::__construct($oImapConfig, $oDevelopments);
		
		$this->_iTagCount = 0;
		$this->_aCapabilityItems = null;
		$this->aObjectConfig['log.literal'] = false;
	}

	/**
	 * @return	void
	 */
	public function __destruct()
	{
		$this->LogoutAndDisconnect();
	}

	/**
	 * @return	bool
	 */
	public function Connect()
	{
		$iPort = $this->oConfig->GetValue('Port', 'int');

		if (parent::Connect(993 === $iPort))
		{
			$aSreverSignature = $this->_readLines('*');
			return $this->_isSuccess($aSreverSignature, '*');
		}
		return false;
	}

	/**
	 * @return	bool
	 */
	public function Login()
	{
		$sLogin = $this->oConfig->GetValue('Login', 'string');
		$sPassword = $this->oConfig->GetValue('Password', 'string');
		
		return $this->_sendRequestWithSuccessReturn('LOGIN', false, array(
			$this->_imapEscape($sLogin, true), $this->_imapEscape($sPassword, true)));
	}

	/**
	 * @return	bool
	 */
	public function ConnectAndLogin()
	{
		return $this->Connect() && $this->Login();
	}

	/**
	 * @return	bool
	 */
	public function Logout()
	{
		return $this->_sendRequestWithSuccessReturn('LOGOUT');
	}

	/**
	 * @return	bool
	 */
	public function LogoutAndDisconnect()
	{
		return $this->Logout() && $this->Disconnect();
	}

	/**
	 * @return	bool
	 */
	public function InitCapability()
	{
		$aCapa = $this->Capability();
		if (is_array($aCapa))
		{
			$this->_aCapabilityItems = $aCapa;
			return true;
		}
		return false;
	}

	/**
	 * @param	string	$sExtentionName
	 * @return	bool
	 */
	public function IsSupport($sExtentionName)
	{
		CCore::CheckTypes(array('$sExtentionName'));

		if (null === $this->_aCapabilityItems)
		{
			$this->InitCapability();
		}

		if (is_array($this->_aCapabilityItems))
		{
			return in_array(strtoupper($sExtentionName), $this->_aCapabilityItems);
		}
		
		return false;
	}

	/**
	 * @return	array
	 */
	public function Capability()
	{
		if ($this->_sendRequest('CAPABILITY'))
		{
			$aTokens = $this->_readLines();
			if ($this->_isSuccess($aTokens))
			{
				$this->_deleteLastToken($aTokens);
				$aReturn = array();
				if (is_array($aTokens) && count($aTokens) > 0)
				{
					foreach ($aTokens as $aToken)
					{
						if ($this->_checkToken($aToken, '*', 'CAPABILITY'))
						{
							for ($i = 2, $c = count($aToken); $i < $c; $i++)
							{
								$aReturn[] = strtoupper($aToken[$i]);
							}
							break;
						}
					}
				}
				return $aReturn;
			}
		}

		return false;
	}

	/**
	 * @return	bool
	 */
	public function Noop()
	{
		return $this->_sendRequestWithSuccessReturn('NOOP');
	}

	/**
	 * @param	string	$sAppendData
	 * @param	string	$sFolderName
	 * @param	array	$naAppendFlags = null
	 * @return	bool
	 */
	public function Append($sAppendData, $sFolderName, $naAppendFlags = null)
	{
		CCore::CheckTypes(array('$sAppendData', '$sFolderName', '$naAppendFlags'));
		
		if ($this->_sendRequest('APPEND', false,
			array($this->_imapEscape($sFolderName, true), $naAppendFlags, '{'.strlen($sAppendData).'}')))
		{
			$bLogLiteral = (bool) $this->GetConfig('log.literal');
			if ($this->_isSuccess($this->_readLines('+'), '+') && $this->putLine($sAppendData, $bLogLiteral))
			{
				if (!$bLogLiteral)
				{
					$this->oDevelopments->Write('#INFO: Appented '.strlen($sAppendData).' bytes.',
						CCoreBaseDevelopments::INFO);
				}
				return $this->_isSuccess($this->_readLines());
			}
		}
		return false;
	}

	/**
	 * @param	string	$sSearchCriterias
	 * @param	bool	$bIsUidCommand
	 * @param	string	$nsCharset = null
	 * @return	array | false
	 */
	public function SimpleSearch($sSearchCriterias, $bIsUidCommand = false, $nsCharset = null)
	{
		CCore::CheckTypes(array('$sSearchCriterias', '$bIsUidCommand', '$nsCharset'));
				
		$aRequest = array();
		if (null !== $nsCharset)
		{
			$aRequest[] = 'CHARSET';
			$aRequest[] = strtoupper($nsCharset);
		}
		
		$aRequest[] = $sSearchCriterias;
		
		if ($this->_sendRequest('SEARCH', $bIsUidCommand, $aRequest))
		{
			$aTokens = $this->_readLines();
			if ($this->_isSuccess($aTokens))
			{
				$this->_deleteLastToken($aTokens);
				$aReturn = array();
				if (count($aTokens) > 0)
				{
					foreach ($aTokens as $aToken)
					{
						if ($this->_checkToken($aToken, '*', 'SEARCH') && count($aToken) > 2)
						{
							for ($i = 2, $c = count($aToken); $i < $c; $i++)
							{
								$aReturn[] = (int) $aToken[$i];
							}
						}
					}
				}
				
				return $aReturn;
			}
		}
		return false;
	}

	/**
	 * @param	array	$aInputFetchItems
	 * @param	string	$sFetchRange
	 * @param	bool	$bIsUidCommand = false
	 * @return	array | false
	 */
	public function Fetch($aInputFetchItems, $sFetchRange, $bIsUidCommand = false)
	{
		CCore::CheckTypes(array('$aInputFetchItems', '$sFetchRange', '$bIsUidCommand'));
				
		$aFetchItems = CCoreSocketImapFetch::ChangeFetchItemsBefourRequest($aInputFetchItems);
		
		if (is_array($aFetchItems) && count($aFetchItems) > 0 &&
				$this->_sendRequest('FETCH', $bIsUidCommand, array((string) $sFetchRange, $aFetchItems)))
		{
			$aTokens = $this->_readLines();
			if ($this->_isSuccess($aTokens))
			{
				$this->_deleteLastToken($aTokens);
				$aReturn = array();
				if (count($aTokens) > 0)
				{
					foreach ($aTokens as $aToken)
					{
						if (4 === count($aToken) && '*' === $aToken[0] && 
							'FETCH' === $aToken[2] && is_array($aToken[3]))
						{
							$iReturnIndex = count($aReturn);
							if (!isset($aReturn[$iReturnIndex]))
							{
								$aReturn[$iReturnIndex] = array();
							}
							$aReturn[$iReturnIndex][CCoreSocketImapFetch::INDEX] = (int) $aToken[1];
							for ($i = 0, $c = count($aToken[3]); $i < $c; $i = $i + 2)
							{
								$aReturn[$iReturnIndex][$aToken[3][$i]] = $aToken[3][$i + 1];
							}
						}
					}
				}
				
				return $aReturn;
			}
		}

		return false;
	}

	/**
	 * @param	array	$aStatusItems
	 * @param	string	$sFolderName = 'INBOX'
	 * @return	array | false
	 */
	public function StatusFolder($aStatusItems, $sFolderName = 'INBOX')
	{
		CCore::CheckTypes(array('$aStatusItems', '$sFolderName'));

		if (is_array($aStatusItems) && count($aStatusItems) > 0 &&
			$this->_sendRequest('STATUS', false, array($this->_imapEscape($sFolderName, true), $aStatusItems)))
		{
			$aTokens = $this->_readLines();
			if ($this->_isSuccess($aTokens))
			{
				$this->_deleteLastToken($aTokens);
				$aReturn = array();
				if (is_array($aTokens) && count($aTokens) > 0)
				{
					foreach ($aTokens as $aToken)
					{
						if ($this->_checkToken($aToken, '*', 'STATUS') &&
								isset($aToken[3]) && is_array($aToken[3]) &&
								count($aToken[3]) == count($aStatusItems) * 2)
						{
							foreach ($aStatusItems as $sStatusItem)
							{
								$iSearchIndex = array_search($sStatusItem, $aToken[3]);
								if (is_integer($iSearchIndex) && isset($aToken[3][$iSearchIndex + 1]))
								{
									$aReturn[$sStatusItem] = $aToken[3][$iSearchIndex + 1];
								}
							}
							return $aReturn;
						}
					}
				}
			}
		}
		
		return false;
	}

	/**
	 * @return	array | false
	 */
	public function ListFolders()
	{
		return $this->_listOrLSub('LIST');
	}

	/**
	 * @return	array | false
	 */
	public function ListSubFolders()
	{
		return $this->_listOrLSub('LSUB');
	}
	
	/**
	 * @param	array	$aInputStoreItems
	 * @param	string	$sStoreRange
	 * @param	string	$sStoreAction
	 * @param	bool	$bIsUidCommand = fasle
	 * @return	bool
	 */
	public function Store($aInputStoreItems, $sStoreRange, $sStoreAction, $bIsUidCommand = false)
	{
		CCore::CheckTypes(array('$aInputStoreItems', '$sStoreRange', '$sStoreAction', '$bIsUidCommand'));
				
		return $this->_sendRequestWithSuccessReturn('STORE', $bIsUidCommand,
			array((string) $sStoreRange, $sStoreAction, $aInputStoreItems));
	}
	
	/**
	 * @param	string	$sFolderName
	 * @return	bool
	 */
	public function CreateFolder($sFolderName)
	{
		CCore::CheckTypes(array('$sFolderName'));
				
		return $this->_sendRequestWithSuccessReturn('CREATE', false,
			array($this->_imapEscape($sFolderName, true)));
	}
	
	/**
	 * @param	string	$sFolderName
	 * @return	bool
	 */
	public function DeleteFolder($sFolderName)
	{
		CCore::CheckTypes(array('$sFolderName'));
		
		return $this->_sendRequestWithSuccessReturn('DELETE', false,
			array($this->_imapEscape($sFolderName, true)));
	}
	
	/**
	 * @param	string	$sOldFolderName
	 * @param	string	$sNewFolderName
	 * @return	bool
	 */
	public function RenameFolder($sOldFolderName, $sNewFolderName)
	{
		CCore::CheckTypes(array('$sOldFolderName', '$sNewFolderName'));

		return $this->_sendRequestWithSuccessReturn('RENAME', false, array(
			$this->_imapEscape($sOldFolderName, true),
			$this->_imapEscape($sNewFolderName, true)));
	}
	
	/**
	 * @param	string	$sFolderName = 'INBOX'
	 * @return	bool
	 */
	public function SelectFolder($sFolderName = 'INBOX')
	{
		CCore::CheckTypes(array('$sFolderName'));

		return $this->_examineOrSelectFolder('SELECT', $sFolderName);
	}
	
	/**
	 * @param	string	$sFolderName = 'INBOX'
	 * @return	bool
	 */
	public function ExamineFolder($sFolderName = 'INBOX')
	{
		CCore::CheckTypes(array('$sFolderName'));

		return $this->_examineOrSelectFolder('EXAMINE', $sFolderName);
	}
	
	/**
	 * @param	string	$sCommand
	 * @return	array | false
	 */
	private function _listOrLSub($sCommand)
	{
		CCore::CheckTypes(array('$sCommand'));

		if (in_array($sCommand, array('LIST', 'LSUB')) && $this->_sendRequest($sCommand, false, array('""', '*')))
		{
			$aTokens = $this->_readLines();
			if ($this->_isSuccess($aTokens))
			{
				$this->_deleteLastToken($aTokens);
				$aReturn = array();
				if (count($aTokens) > 0)
				{
					foreach ($aTokens as $aToken)
					{
						if ($this->_checkToken($aToken, '*', $sCommand, 5))
						{
							$aReturn[] = array($aToken[4], $aToken[3], $aToken[2]);
						}
					}
				}
				return $aReturn;
			}
		}
		return false;
	}

	/**
	 * @param	string	$sCommand = 'SELECT'
	 * @param	string	$sFolderName = 'INBOX'
	 * @return	bool
	 */
	private function _examineOrSelectFolder($sCommand = 'SELECT', $sFolderName = 'INBOX')
	{
		CCore::CheckTypes(array('$sCommand', '$sFolderName'));

		return in_array($sCommand, array('EXAMINE', 'SELECT')) &&
			$this->_sendRequestWithSuccessReturn($sCommand, false, array($this->_imapEscape($sFolderName, true)));
	}

	/**
	 * @param	string	$sCommand
	 * @param	bool	$bIsUidCommand = false
	 * @param	array	$naParams = null
	 * @return	bool
	 */
	private function _sendRequestWithSuccessReturn($sCommand, $bIsUidCommand = false, $naParams = null)
	{
		CCore::CheckTypes(array('$sCommand', '$bIsUidCommand', '$naParams'));
		
		if ($this->_sendRequest($sCommand, $bIsUidCommand, $naParams))
		{
			return $this->_isSuccess($this->_readLines());
		}
		return false;
	}

	/**
	 * @param	string	$sCommand
	 * @param	bool	$bIsUidCommand = false
	 * @param	array	$naParams = null
	 * @return	bool
	 */
	private function _sendRequest($sCommand, $bIsUidCommand = false, $naParams = null)
	{
		CCore::CheckTypes(array('$sCommand', '$bIsUidCommand', '$naParams'));

		$sCommand = ($bIsUidCommand) ? 'UID '.$sCommand : $sCommand;
		return $this->putLine(
			$this->_getNewTag().' '.$sCommand.$this->_prepearParamLine($naParams));
	}

	/**
	 * @param	array	$naParams = null
	 * @return	string
	 */
	private function _prepearParamLine($naParams = null)
	{
		CCore::CheckTypes(array('$naParams'));
		
		$sReturn = '';
		if (null !== $naParams && count($naParams) > 0)
		{
			foreach ($naParams as $mParamItem)
			{
				if (is_array($mParamItem) && count($mParamItem) > 0)
				{
					$sReturn .= ' ('.trim($this->_prepearParamLine($mParamItem)).')';
				}
				else if (is_string($mParamItem))
				{
					$sReturn .= ' '.$mParamItem;
				}
			}
		}
		return $sReturn;
	}

	/**
	 * @return	array
	 */
	private function _readLine()
	{
		return $this->_decodeLine($this->getLine());
	}

	/**
	 * @param	string	$sEndTag = null
	 * @return	array
	 */
	private function _readLines($nsEndTag = null)
	{
		CCore::CheckTypes(array('$nsEndTag'));
		
		$aLines = array();
		do
		{
			$aTokens = $this->_readLine();
			$aLines[] = $aTokens;
			if ($this->_isEndTag($aTokens, $nsEndTag))
			{
				break;
			}
		}
		while (true);

		return $aLines;
	}
	
	/**
	 * @return	string
	 */
	private function _getNewTag()
	{
		$this->_iTagCount++;
		return $this->_getCurrentTag();
	}

	/**
	 * @return	string
	 */
	private function _getCurrentTag()
	{
		return self::TAG_NAME.$this->_iTagCount;
	}

	/**
	 * @param	array	$aLines
	 * @param	string	$nsEndTag
	 * @return	bool
	 */
	private function _isSuccess($aLines, $nsEndTag = null)
	{
		CCore::CheckTypes(array('$aLines', '$nsEndTag'));
		
		if (is_array($aLines) && count($aLines) > 0)
		{
			$aLine = $aLines[count($aLines) - 1];
			if ($this->_isEndTag($aLine, $nsEndTag) && $this->_isOkToken($aLine))
			{
				return true;
			}

			$this->oDevelopments->Write('error last line response.',
				CCoreBaseDevelopments::WARNING);
		}
		else
		{
			$this->oDevelopments->Write('null response.',
				CCoreBaseDevelopments::WARNING);
		}

		return false;
	}

	/**
	 * @param	array	$aToken
	 * @param	string	$sFirstItem
	 * @param	string	$sSecondItem
	 * @param	int		$niCountCheck = null
	 * @return	bool
	 */
	private function _checkToken($aToken, $sFirstItem, $sSecondItem, $niCountCheck = null)
	{
		CCore::CheckTypes(array('$aToken', '$sFirstItem', '$sSecondItem', '$niCountCheck'));
		
		$return = is_array($aToken);
		$return &= (null !== $niCountCheck && $niCountCheck > 1)
			? $niCountCheck === count($aToken) : count($aToken) > 1;
		return ($return && $sFirstItem === $aToken[0] && $sSecondItem === $aToken[1]);
	}

	/**
	 * @param	array	$aToken
	 * @param	string	$nsCheckTag = null
	 * @return	bool
	 */
	private function _isEndTag($aToken, $nsCheckTag = null)
	{
		CCore::CheckTypes(array('$aToken', '$nsCheckTag'));

		$nsCheckTag = (null !== $nsCheckTag) ? $nsCheckTag : $this->_getCurrentTag();
		return (false !== $aToken && isset($aToken[0]) && $aToken[0] === $nsCheckTag);
	}

	/**
	 * @param	array	$aToken
	 * @return	bool
	 */
	private function _isOkToken($aToken)
	{
		CCore::CheckTypes(array('$aToken'));
		
		return (false !== $aToken && isset($aToken[1]) && $aToken[1] === 'OK');
	}

	/**
	 * @param	array	$aToken
	 * @return	void
	 */
	private function _deleteLastToken(&$aToken)
	{
		CCore::CheckTypes(array('$aToken'));
		
		if (is_array($aToken) && count($aToken) > 0)
		{
			unset($aToken[count($aToken) - 1]);
		}
	}

	/**
	 * @param	string	$line
	 * @return	array
	 */
	private function _decodeLine($line)
	{
		$bLogLiteral = (bool) $this->GetConfig('log.literal');

		$tokens = array();
        $stack = array();

        if (false === $line)
        {
        	return $tokens;
        }

        /**
            We start to decode the response here. The unterstood tokens are:
                literal
                "literal" or also "lit\\er\"al"
                {bytes}<NL>literal
                (literals*)
            All tokens are returned in an array. Literals in braces (the last unterstood
            token in the list) are returned as an array of tokens. I.e. the following response:
                "foo" baz {3}<NL>bar ("f\\\"oo" bar)
            would be returned as:
                array('foo', 'baz', 'bar', array('f\\\"oo', 'bar'));

            TODO: add handling of '[' and ']' to parser for easier handling of response text
        */
        //  replace any trailling <NL> including spaces with a single space
        $line = rtrim($line) . ' ';
        while (($pos = strpos($line, ' ')) !== false) {
            $token = substr($line, 0, $pos);
            while (isset($token[0]) && $token[0] == '(') {
                array_push($stack, $tokens);
                $tokens = array();
                $token = substr($token, 1);
            }
            if (isset($token[0]) && $token[0] == '"') {
                //if (preg_match('%^"((.|\\\\|\\")*?)" *%', $line, $matches))
				if (preg_match('%^\(*"((.|\\\\|\\")*?)" *%', $line, $matches))
				{
                    $tokens[] = $matches[1];
                    $line = substr($line, strlen($matches[0]));
                    continue;
                }
            }
            if (isset($token[0]) && $token[0] == '{') {
                $endPos = strpos($token, '}');
                $chars = substr($token, 1, $endPos - 1);
                if (is_numeric($chars)) {
                    $token = '';
                    while (strlen($token) < $chars) {
                        $token .= $this->getLine($bLogLiteral);
                    }
					if (!$bLogLiteral)
					{
						$this->oDevelopments->Write('#INFO: Received '.strlen($token).' bytes.',
							CCoreBaseDevelopments::INFO);
					}
                    $line = '';
                    if (strlen($token) > $chars) {
                        $line = substr($token, $chars);
                        $token = substr($token, 0, $chars);
                    } else {
                        $line .= $this->getLine();
                    }
					
					
                    $tokens[] = $token;
                    $line = trim($line) . ' ';
                    continue;
                }
            }
            if ($stack && $token[strlen($token) - 1] == ')') {
                // closing braces are not seperated by spaces, so we need to count them
                $braces = strlen($token);
                $token = rtrim($token, ')');
                // only count braces if more than one
                $braces -= strlen($token) + 1;
                // only add if token had more than just closing braces
                if (rtrim($token) != '') {
                    $tokens[] = rtrim($token);
                }
                $token = $tokens;
                $tokens = array_pop($stack);
                // special handline if more than one closing brace
                while ($braces-- > 0) {
                    $tokens[] = $token;
                    $token = $tokens;
                    $tokens = array_pop($stack);
                }
            }
            $tokens[] = $token;
            $line = substr($line, $pos + 1);
        }

        // maybe the server forgot to send some closing braces
        while ($stack) {
            $child = $tokens;
            $tokens = array_pop($stack);
            $tokens[] = $child;
        }

        return $tokens;
	}

	/**
	 * @param	string	$sLineForEscape
	 * @param	bool	$bAddQuot = false
	 * @return	string
	 */
	private function _imapEscape($sLineForEscape, $bAddQuot = false)
	{
		CCore::CheckTypes(array('$sLineForEscape', '$bAddQuot'));
		
		$sReturn = strtr($sLineForEscape, array('"' => '\\"', '\\' => '\\\\'));
		return ($bAddQuot) ? '"'.$sReturn.'"' : $sReturn;
	}
}
